package pl.edu.agh.mobsys;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.garret.perst.Storage;
import org.jivesoftware.smack.AccountManager;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.xmlpull.v1.XmlPullParser;

import pl.edu.agh.mobsys.data.ConnectionConfig;
import pl.edu.agh.mobsys.data.Contact;
import pl.edu.agh.mobsys.data.ContactFactory;
import pl.edu.agh.mobsys.data.Message;
import pl.edu.agh.mobsys.data.PersonalContact;
import pl.edu.agh.mobsys.data.SmoaContact;
import pl.edu.agh.mobsys.data.validation.Validator;
import pl.edu.agh.mobsys.persistence.MessageDataHelper;
import pl.edu.agh.mobsys.soap.JobData;
import pl.edu.agh.mobsys.utils.Constants;
import pl.edu.agh.mobsys.utils.SmoaParser;
import pl.edu.agh.mobsys.utils.Constants.SmoaAgentType;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.util.Log;

/**
 * Klasa aplikacji, globalne informacje dla aplikacji
 * 
 * do pobrania w activity poprzez (XmppActivity)getApplication();
 * */
public class XmppApplication extends Application {

	private static final String TAG = "XmppApplication";

	private XMPPConnection con = null;

	private pl.edu.agh.mobsys.utils.ConnectionListener connectionListener;

	private final Connector connector = new Connector();

	private PacketListener jobsPacketListener;

	// private PacketListener addJobPacketListener;

	/**
	 * @return {@link ConnectionListener} callback dla statusu połączenia z
	 *         serwerem XMPP
	 * */
	public pl.edu.agh.mobsys.utils.ConnectionListener getConnectionListener() {
		return connectionListener;
	}

	/**
	 * Rejestracja callback'a {@link ConnectionListener} dla monitorowania
	 * statusu połączenia z serwerem XMPP
	 * */
	public void setConnectionListener(
			pl.edu.agh.mobsys.utils.ConnectionListener connectionListener) {
		this.connectionListener = connectionListener;
	}

	/**
	 * @return aktualna instancja połaczenia z serwerem XMPP
	 * */
	public XMPPConnection getXMPPConnection() {
		return con;
	}

	/**
	 * @return czy jest połączenie z serwerem XMPP
	 * */
	public boolean isConnected() {
		return con != null && con.isConnected() && con.isAuthenticated();
	}

	/**
	 * Zerwanie połaczenia i ponowna próba
	 * 
	 * @param handler
	 *            do odbierania asynchronicznych powiadomień o stanie
	 *            nawiązywanego połączenia
	 * */
	public void reconnect(Handler handler) {
		if (con != null) {
			try {
				con.disconnect();
			} catch (Exception e) {
				; // no handling - ok
			}
			con = null;
		}
		tryConnect(handler);
	}

	private Handler handler;
	private boolean hold = false;

	/**
	 * Wysłanie do zajstrowanego handlera wiadomości z zasobu string o id
	 * <code>mesgId</code>
	 * 
	 * @param mesgId
	 *            - id wiadomości do wysłania
	 * */
	private void show(int mesgId) {
		show(getResources().getString(mesgId));
	}

	/**
	 * Wysłanie do zajstrowanego handlera wiadomości <code>mesg</code>
	 * 
	 * @param mesg
	 *            - wiadomość do wysłania
	 * */
	private void show(final String mesg) {
		if (hold) {
			return;
		}
		android.os.Message m = new android.os.Message();
		m.obj = mesg;
		if (handler != null) {
			handler.sendMessage(m);
			try {
				Thread.sleep(600); // żeby dało się przeczytać
			} catch (InterruptedException e) {
				;
			}
		}
	}

	public void holdConnecting() {
		hold = true;
		show(null);
	}

	private Thread t;

	/**
	 * Próba nawiązania połączenia, o ile nie istnieje
	 * 
	 * @param handler
	 *            do odbierania asynchronicznych powiadomień o stanie
	 *            nawiązywanego połączenia
	 * */
	public synchronized void tryConnect(Handler handler) {
		try {
			if (con == null) {

				// rejestacja handlera
				this.handler = handler;

				// jeśli wątek connectora działa to go aktywujemy
				// od teraz będzie wysyłał powiadomienia do handlera
				if (t != null && t.isAlive()) {
					hold = false;

					// jeśli nie to uruchamiany jest nowy wątek connectora
				} else {
					t = new Thread(connector);
					t.start(); // asynchronicznie
				}
			}
		} catch (ThreadDeath e) {
			;
		} catch (Exception e) {
			show(R.string.con_init_connection_failed + e.getMessage());
			e.printStackTrace();
		}
	}

	/**
	 * Wątek realizujący połączenie z serwerem XMPP.
	 * */
	class Connector implements Runnable {

		public void run() {
			try {
				show(R.string.con_init_connecting);

				// odczyt konfiguracji z preferencji
				ConnectionConfig cc = ConnectionConfig
						.readFromSharedPreferences(getSharedPreferences(
								Constants.SHARED_PREFS_KEY_SERVER_CONFIG,
								Context.MODE_WORLD_READABLE));

				// walidacja czy dane w ogóle mają sens
				final String warning = Validator
						.validateServerConfiguration(cc);

				// jeśli nie to
				if (warning != null) {
					// połączenie nieaktywne
					connectionListener.connectionInactive(warning);

					// wyświetlenie ostrzeżenia
					show(warning);
					waitAWhile();
					show(null);

					// nic więcej się nie da zrobić
					return;
				}

				ConnectionConfiguration config = new ConnectionConfiguration(
						cc.ip, Integer.parseInt(cc.port), cc.host);

				// dla pewności wyłaczamy jakiekolwiek zabezpieczenia
				config.setSASLAuthenticationEnabled(false);
				config.setSecurityMode(SecurityMode.disabled);

				con = new XMPPConnection(config);

				// próba połączenia - synchroniczne!
				con.connect();

				if (con.isConnected()) {

					// dodanie specjalnego modułu parsera, aby uzyskać envelopy
					// soapa
					addSoapParserExtension(con);

					con.addConnectionListener(new ConnectionListener() {

						public void reconnectionSuccessful() {
							connectionListener.connectionActive();
						}

						public void reconnectionFailed(Exception arg0) {
							connectionListener.connectionInactive(arg0
									.getLocalizedMessage());
						}

						public void reconnectingIn(int arg0) {
							connectionListener.connectionInactive("");
						}

						public void connectionClosedOnError(Exception arg0) {
							connectionListener.connectionInactive(arg0
									.getLocalizedMessage());
						}

						public void connectionClosed() {
							connectionListener.connectionInactive("");
						}
					});

					// połączenie aktywne
					connectionListener.connectionActive();

					// połączony
					show(R.string.con_init_authenticating);
					try {
						// logowanie z resourcem
						if (cc.resource != null && cc.resource != "") {
							con.login(cc.username, cc.password, cc.resource);
							Log.d(TAG, "Logged in: " + cc.username + " "
									+ cc.password + " " + cc.resource);
							// lub bez
						} else {
							con.login(cc.username, cc.password);
							Log.d(TAG, "Logged in: " + cc.username + " "
									+ cc.password);
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
					if (con.isAuthenticated()) {
						// zalogowany
						show(R.string.con_init_authenticated);
					} else {
						// niezalogowany
						AccountManager am = con.getAccountManager();
						if (!am.supportsAccountCreation()) {
							// nie da się założyć konta
							show(R.string.con_init_authentication_failed);
							waitAWhile();
						} else {
							// da się założyć konto - próba
							show(R.string.con_init_account_creating);
							am.createAccount(cc.username, cc.password,
									new HashMap<String, String>());
							con.login(cc.username, cc.password);
							if (con.isAuthenticated()) {
								// udało się stworzyć konto i zalogować
								show(R.string.con_init_authenticated);
							} else {
								// jednak nie udało się zalogować
								show(R.string.con_init_authentication_failed);
								waitAWhile();
							}
						}
					}
				} else {
					if (hold) {
						// wątek connectora został wstrzymany
						// zatrzymujemy wszystko i sprzątamy
						hold = false;
						return;
					}
					// niepołączony
					show(R.string.con_init_connection_failed);
					connectionListener.connectionInactive("");
					waitAWhile();
				}
			} catch (Exception e) {
				if (hold) {
					// wątek connectora został wstrzymany
					// zatrzymujemy wszystko i sprzątamy
					hold = false;
					return;
				}

				// wyjątek - wyświetlamy
				show(getResources().getString(
						R.string.con_init_connection_failed)
						+ ": " + e.getLocalizedMessage());
				waitAWhile();
				e.printStackTrace();
				con = null;
			} finally {
				try {
					// żeby zdążyć przeczytać ostatni komunikat
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				show(null);
			}
		}

		/**
		 * Z racji ograniczeń biblioteki SMACK, aby obsługiwać pewne szczególne
		 * zawartości wiadomości należy dodać customowe ExtensionProvidery. Ten
		 * dodany poniżej pozwala parsować wiadomości SOAP.
		 * 
		 * Gdy określony tag XML'a zostanie napotkany przez wewnętrzny parser
		 * SMACKA (MxParser) przeglądana jest lista Extension providerów, które
		 * mogą go parsować. Domyślnie "Envelope" nie ma żadnego extension
		 * providera i jest pomijane, bądź częściowo parsowane i w rezultacie
		 * zwaracane jako property obiektu Message.
		 * */
		private void addSoapParserExtension(XMPPConnection con) {

			final PacketExtensionProvider pep = new PacketExtensionProvider() {

				public PacketExtension parseExtension(XmlPullParser parser)
						throws Exception {

					final StringBuilder sb = new StringBuilder();

					// provider zacznie działać, gdy parser stanie na elemencie
					// "Envelope" - chodzi o to, żeby przechwycić całą zawartość
					// tagu włącznie z nim samym
					while (true) {
						sb.append(parser.getText());
						int eventType = parser.next();
						// do momentu zakończenie elementu "Envelope"
						// przepisuje wszystko i zwracamy
						if (eventType == XmlPullParser.END_TAG
								&& parser.getName().equals("Envelope")) {
							sb.append(parser.getText());
							break;
						}
					}
					return new PacketExtension() {

						public String getElementName() {
							return "Envelope";
						}

						public String getNamespace() {
							return SmoaParser.SOAP_NAMESPACE;
						}

						public String toXML() {
							return sb.toString();
						}
					};
				}
			};

			// bardzo ważne
			// rejestracja dla tylko jednej nazwy tagu nie działa
			// dlatego dla dwóch tagów rejestrujemy ten sam handler
			ProviderManager.getInstance().addExtensionProvider(
					SmoaParser.SOAP_ELEMENT_NAME_2, SmoaParser.SOAP_NAMESPACE,
					pep);
			ProviderManager.getInstance().addExtensionProvider(
					SmoaParser.SOAP_ELEMENT_NAME_1, SmoaParser.SOAP_NAMESPACE,
					pep);
		}

		/**
		 * Czeka kilka sekund, aby poprzedni komunikat nie został nadpisany.
		 * */
		private void waitAWhile() {
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				; // ok
			}
		}
	}

	/**
	 * Odłączenie od serwera XMPP.
	 * */
	public void disconnect() {
		if (con != null) {
			con.disconnect();
		}
		connectionListener.connectionInactive("");
	};

	@Override
	public void onTerminate() {
		disconnect();
		if (db != null) {
			db.close();
		}
		super.onTerminate();
	}

	/**
	 * @param alias
	 *            - alias do żądanego kontaktu
	 * @return - Contact dla zadanego aliasu
	 * */
	public Contact getContactByAlias(String alias) {
		return ContactFactory.buildContact(alias, getSharedPreferences(
				Constants.SHARED_PREFS_KEY_CONTACTS,
				Context.MODE_WORLD_READABLE).getString(alias, null));
	}

	// cache z chatami
	private final Map<String, Chat> chats = new HashMap<String, Chat>();

	/**
	 * Umożliwia pobieranie chatów z użytkownikami z listy.
	 * 
	 * @param alias
	 *            - alias użytkownika dla którego chat chcemy pobrać
	 * @param type
	 *            - jeśli == null to pobiera chat dla zwykłego agenta (nie
	 *            smoa), jeśli != null to wskazuje na pobranie odpowiedniego
	 *            chatu dla danego aliasu agenta smoa.
	 * 
	 * @return żądany chat, null jeśli nie istnieje podany kontakt.
	 * */
	public Chat getChat(String alias, SmoaAgentType type) {

		Chat c;

		// pobranie kontaktu z preferencji
		Contact ct = getContactByAlias(alias);

		// jeśli kontakt osobowy - nie smoa
		if (type == null && ct instanceof PersonalContact) {
			PersonalContact pc = (PersonalContact) ct;
			c = chats.get(pc.getAlias());

			// w cachu nie ma chatu - tworzenie
			if (c == null) {
				c = con.getChatManager().createChat(
						pc.getXmppName()
								+ "@"
								+ con.getServiceName()
								+ (pc.getResource() == null ? "" : "/"
										+ pc.getResource()), null);
				chats.put(alias, c);
			}

			// jeśli kontakt typu smoa
		} else if (type != null && ct instanceof SmoaContact) {
			SmoaContact sc = (SmoaContact) ct;

			final String username = type
					.equals(Constants.SmoaAgentType.JOB_RECEIVER) ? sc
					.getJobReceiverXmppName() : sc.getNotifierXmppName();

			Log.d(TAG, "Searching SMOA chat for: " + username);

			c = chats.get(username);

			// w cachu nie ma chatu - tworzenie
			if (c == null) {
				final String resource = type
						.equals(Constants.SmoaAgentType.JOB_RECEIVER) ? sc
						.getJobReceiverResource() : sc.getNotifierResource();

				c = con.getChatManager().createChat(
						username + "@" + con.getServiceName()
								+ (resource == null ? "" : "/" + resource),
						null);
				chats.put(username, c);
			}
		} else {
			System.out.println("Chat for \"" + alias
					+ "\" not created (type = " + type == null ? "not SMOA"
					: type + ")");
			return null;
		}
		Log.d(TAG, "Returning chat for participant: " + c.getParticipant());
		return c;
	}

	private final Map<String, Map<String, JobData>> jobsMap = new HashMap<String, Map<String, JobData>>();

	public Map<String, JobData> getJobs(String alias) {
		// if(db == null) {
		// createStorage();
		// }
		Map<String, JobData> jobs = jobsMap.get(alias);
		if (jobs == null) {
			jobs = new HashMap<String, JobData>();
			jobsMap.put(alias, jobs);
		}
		return jobs;
	}

	private final Map<String, List<Message>> messagesMap = new HashMap<String, List<Message>>();
	private final Map<String, MessageDataHelper> dataHelpers = new HashMap<String, MessageDataHelper>();

	public List<Message> getMessages(String alias) {
		List<Message> messagesList = messagesMap.get(alias);
		if (messagesList == null) {
			MessageDataHelper dataHelper = dataHelpers.get(alias);
			if (dataHelper == null) {
				dataHelper = new MessageDataHelper(this, alias);
			}
			dataHelpers.put(alias, dataHelper);
			messagesList = dataHelper.selectAll();
			messagesMap.put(alias, messagesList);
		}
		return messagesList;
	}

	public MessageDataHelper getMessageDataHelper(String alias) {
		return dataHelpers.get(alias);
	}

	public void setJobsPacketListener(PacketListener jobsPacketListener) {
		this.jobsPacketListener = jobsPacketListener;
	}

	public PacketListener getJobsPacketListener() {
		return jobsPacketListener;
	}

	// public void setAddJobPacketListener(PacketListener addJobPacketListener)
	// {
	// this.addJobPacketListener = addJobPacketListener;
	// }
	//
	// public PacketListener getAddJobPacketListener() {
	// return addJobPacketListener;
	// }

	private Storage db;
}
